/*
 * File     : LibraryHelper.java
 * Created  : 22 Apr 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.library;

import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.LongBuffer;
import java.nio.ShortBuffer;
import java.util.logging.Level;

import com.googlecode.dni.Warning;
import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.internal.NativeUnsafeAccess;
import com.googlecode.dni.type.Pointer;


/**
 * <p>
 *  Provides helper functions for library calls.
 * </p>
 *
 * @author Matthew Wilson
 */
public final class LibraryHelper
{

    private static final ByteOrder NATIVE_ORDER = ByteOrder.nativeOrder();

    // TODO KILL all this scratch stuff
    private static final ThreadLocal< Cache > SCRATCH_LOCAL = new ThreadLocal< Cache >()
    {
        @Override
        protected Cache initialValue()
        {
            return new Cache( 0 );
        }
    };

    // TODO could be "faster" if using just native pointers...

    public static ByteBuffer getScratch( final int size )
    {
        Cache cache = SCRATCH_LOCAL.get();
        ByteBuffer buffer = cache.buffer;
        if ( buffer.remaining() < size )
        {
            cache = new Cache( size );
            buffer = cache.buffer;
            SCRATCH_LOCAL.set( cache );
        }

        int end = buffer.position() + size;
        buffer.limit( end );
        ByteBuffer slice = buffer.slice();
        slice.order( ByteOrder.nativeOrder() );

        buffer.limit( buffer.capacity() ).position( end );

        return slice;
    }

    // TODO this is a confusing API!
    public static Object reserveScratch( final int size )
    {
        Cache cache = SCRATCH_LOCAL.get();
        ByteBuffer buffer = cache.buffer;
        if ( buffer.remaining() < size )
        {
            cache = new Cache( size );
            buffer = cache.buffer;
            SCRATCH_LOCAL.set( cache );
        }

        return cache;
    }

    public static long getScratchPointer( final Object scratch, final int size )
    {
        return ( (Cache) scratch ).getPointer() - size;
    }

    public static ByteBuffer getScratchBuffer( final Object scratch, final int size )
    {
        Cache cache = (Cache) scratch;

        ByteBuffer buffer = cache.buffer;

        int end = buffer.position() + size;
        buffer.limit( end );

        ByteBuffer slice = buffer.slice().order( ByteOrder.nativeOrder() );

        buffer.limit( buffer.capacity() ).position( end );

        return slice;
    }

    public static void releaseScratch( final int size )
    {
        Cache cache = SCRATCH_LOCAL.get();
        cache.release( size );
    }

    public static long toNativeString( final String string )
    {
//
//        int size = string.length() * 2 + 1;
//        Cache cache = SCRATCH_LOCAL.get();
//        ByteBuffer buffer = cache.buffer;
//        if ( buffer.remaining() < size )
//        {
//            cache = new Cache( size );
//            buffer = cache.buffer;
//            SCRATCH_LOCAL.set( cache );
//        }
//
//        long ptr = cache.getPointer();
//        int end = buffer.position() + size;
//        buffer.position( end );
//
//        byte[] array = new byte[ string.length() + 1 ];
//        for ( int i = 0; i < string.length(); i++ )
//        {
//            array[ i ] = (byte) string.charAt( i );
//        }
//        NativeUnsafeAccess.copyFromArray( ptr, array, array.length );
//
////        scratch.put( string.getBytes() ).put( (byte) 0 ).clear();
//
//        return ptr;

//        System.out.println( "toNativeString: " + string );

        // TODO Argh!
        long pointer = NativeUnsafeAccess.getStringUTFChars( string );

//        System.out.println( "toNativeString: " + string + " " + new Pointer( pointer ) );
        return pointer;
    }

    public static void releaseNativeString( final String string, final long pointer )
    {
//        System.out.println( "releaseNativeString: " + string + " " + new Pointer( pointer ) );
        if ( pointer == 0 )
        {
            return;
        }

        // TODO Argh!
        NativeUnsafeAccess.releaseStringUTFChars( string, pointer );
    }

    /**
     * Checks whether the user-provided buffer is of the expected size.
     *
     * @param buffer
     *            the buffer
     * @param expectedSize
     *            the expected size
     */
    public static void checkFixedSizeBuffer( final Buffer buffer,
                                             final int expectedSize )
    {
        if ( buffer.capacity() != expectedSize )
        {
            DniInternal.LOGGER.log( Level.WARNING,
                                    "Incorrectly sized buffer passed; expected " + expectedSize
                                        + " but got " + buffer,
                                    new Warning() );
        }
    }

    /**
     * Wraps the given pointer in a buffer.
     *
     * @param pointer
     *            the pointer
     * @param sizeInBytes
     *            the size in bytes
     * @return a buffer
     */
    public static ByteBuffer getByteBuffer( final long pointer,
                                            final int sizeInBytes )
    {
        return NativeUnsafeAccess.getBuffer( pointer, sizeInBytes ).order( NATIVE_ORDER );
    }

    /**
     * Wraps the given pointer in a buffer.
     *
     * @param pointer
     *            the pointer
     * @param sizeInBytes
     *            the size in bytes
     * @return a buffer
     */
    public static CharBuffer getCharBuffer( final long pointer,
                                            final int sizeInBytes )
    {
        return NativeUnsafeAccess.getBuffer( pointer, sizeInBytes ).order( NATIVE_ORDER ).asCharBuffer();
    }

    /**
     * Wraps the given pointer in a buffer.
     *
     * @param pointer
     *            the pointer
     * @param sizeInBytes
     *            the size in bytes
     * @return a buffer
     */
    public static ShortBuffer getShortBuffer( final long pointer,
                                              final int sizeInBytes )
    {
        return NativeUnsafeAccess.getBuffer( pointer, sizeInBytes ).order( NATIVE_ORDER ).asShortBuffer();
    }

    /**
     * Wraps the given pointer in a buffer.
     *
     * @param pointer
     *            the pointer
     * @param sizeInBytes
     *            the size in bytes
     * @return a buffer
     */
    public static IntBuffer getIntBuffer( final long pointer,
                                           final int sizeInBytes )
    {
        return NativeUnsafeAccess.getBuffer( pointer, sizeInBytes ).order( NATIVE_ORDER ).asIntBuffer();
    }

    /**
     * Wraps the given pointer in a buffer.
     *
     * @param pointer
     *            the pointer
     * @param sizeInBytes
     *            the size in bytes
     * @return a buffer
     */
    public static LongBuffer getLongBuffer( final long pointer,
                                            final int sizeInBytes )
    {
        return NativeUnsafeAccess.getBuffer( pointer, sizeInBytes ).order( NATIVE_ORDER ).asLongBuffer();
    }

    /**
     * Wraps the given pointer in a buffer.
     *
     * @param pointer
     *            the pointer
     * @param sizeInBytes
     *            the size in bytes
     * @return a buffer
     */
    public static FloatBuffer getFloatBuffer( final long pointer,
                                              final int sizeInBytes )
    {
        return NativeUnsafeAccess.getBuffer( pointer, sizeInBytes ).order( NATIVE_ORDER ).asFloatBuffer();
    }

    /**
     * Wraps the given pointer in a buffer.
     *
     * @param pointer
     *            the pointer
     * @param sizeInBytes
     *            the size in bytes
     * @return a buffer
     */
    public static DoubleBuffer getDoubleBuffer( final long pointer,
                                                final int sizeInBytes )
    {
        return NativeUnsafeAccess.getBuffer( pointer, sizeInBytes ).order( NATIVE_ORDER ).asDoubleBuffer();
    }

    public static void pushVariableArityArguments( final Object scratch,
                                                   final Object[] args )
    {
        getScratchBuffer( scratch, args.length * 8 );
        // TODO ...
    }

    private static final class Cache
    {

        private final ByteBuffer buffer;

        private final long pointer;

        /**
         * Creates a new instance.
         */
        Cache( final int size )
        {
            this.buffer = ByteBuffer.allocateDirect( Math.max( 65536, size ) );
            this.pointer = Pointer.fromBuffer( this.buffer ).getAddress();
        }

        /**
         *
         * @return
         */
        long getPointer()
        {
            return this.pointer + this.buffer.position();
        }

        /**
         * @param size
         *            the number of bytes
         */
        void release( final int size )
        {
            this.buffer.position( Math.max( 0, this.buffer.position() - size ) );
        }

    }

}
